package hangxingliu.java.yddictapi;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;

import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

/**
 * 有道查词类
 * 
 * @author VoyageLiu
 * @version 2.0
 */
public class YdDict {
	
	/**
	 * 访问有道WAP词典使用的UserAgent
	 */
	private static final String HTTP_USERAGENT = "Opera";
	
	/**
	 * 词典查询的网址(用%s表示需要查询的单词)
	 */
	private static final String BASE_QUERY_URL = "http://dict.youdao.com/m/search?keyfrom=dict.mindex&q=%s";
	
	/**
	 * 词典查询详细内容的网址(用第一个%s表示需要查询的词典的缩写,第二个%s表示需要查询的单词)
	 */
	private static final String BASE_QUERY_DETAILED_URL = "http://m.youdao.com/singledict?more=false&le=eng&dict=%s&q=%s";
	
	/**
	 * 获得读音的网址(用%s表示需要查询的单词,%d表示哪个国家的发音)
	 */
	private static final String BASE_QUERY_PRONOUNCE_URL = "http://dict.youdao.com/dictvoice?audio=%s&type=%d";
	
	/**
	 * 查询单词的字符编码(用于转换成URL码)
	 */
	private static final String QUERY_URLENCODER_CHARSET = "UTF-8";
	
	/**
	 * 数据块类别所在div的class属性
	 */
	private static final String CATEGORY_CLASS = "category";
	
	/**
	 * 数据块内容所在div的class属性
	 */
	private static final String CONTENT_CLASS = "content";
	
	/**
	 * 当无法翻译的时候第一个数据块的类别
	 */
	private static final String ERROR_CATEGORY = "出错";
	
	/**
	 * 翻译结果中"基本翻译"的类别名
	 */
	private static final String BASIC_TRANSLATE = "基本翻译";
	
	/**
	 * 字母和读音所在的Div的编号(从0开始)
	 */
	private static final int WORD_AND_PRON_DIV = 2;
	
	/**
	 * 无效读音的返回数据长度小于这个值
	 */
	private static final int PRONOUNCE_VOID_LENGTH = 576 + 100;
	
	/**
	 * 无效的私有构建方法
	 */
	private YdDict(){
		//用于隐藏公共构建方法
	}
	
	/**
	 * 通过有道WAP词典方式翻译单词或中文
	 * @param word 需要翻译的单词或中文
	 * @return 翻译结果
	 */
	public static YdDictResponse translate(String word){
		//返回的结果的对象
		YdDictResponse res = null;
		
		//去除需要翻译内容的无效字符
		word = word.trim();
		//无查询内容
		if(word.length() == 0){
			res = new YdDictResponse(new String[]{},
					new String[]{}, new String[]{}, 0);
			return res;
		}
		
		//获得查询URL
		String url = genQueryURL(word);
		//获得JSoup可解析文档
		Document doc = genJSoupDocument(url);
		if(doc == null){
			res = new YdDictResponse("网络连接错误!");
			return res;
		}
		
		//解析各个数据块
		Elements categorys = doc.getElementsByClass(CATEGORY_CLASS);
		Elements contents = doc.getElementsByClass(CONTENT_CLASS);
		//数据块长度
		int categoryLength = categorys.size();
		int contentLength = contents.size();
		//当没有返回数据块的时候
		if(categoryLength == 0 || contentLength == 0){
			res = new YdDictResponse("网页异常错误!(翻译返回数据的数据类别数为0)");
			return res;
		}

		//得到第一个区块类别
		Element categoryFirst = categorys.first();
		String cgfirst = categoryFirst.text();
		//无法得到类别名
		if(cgfirst == null){
			res = new YdDictResponse("网页异常错误!(翻译返回数据的第一个区块的类别为空)");
			return res;
		}
		//去除空白字符
		cgfirst = cgfirst.trim();
		//翻译出错,找不到此关键字的翻译结果
		if(cgfirst.equals(ERROR_CATEGORY)){
			//获得官方的出错提示语
			Element contentFirst = contents.first();
			res = new YdDictResponse(new String[]{cgfirst},
					new String[]{contentFirst.text()}, new String[]{contentFirst.html()},
					1);
			return res;
		}
		
		//各个数据块数组
		String[] blockName = new String[categoryLength];
		String[] blockData = new String[categoryLength];
		String[] blockHTMLData = new String[categoryLength];
		//写入数据的指针
		int writeBlockp = 0;
		
		//如果第一项是基本翻译
		if(cgfirst.startsWith(BASIC_TRANSLATE)){
			//不让数据块类别名直接被赋为cgfirst的原因是cgfirst的值实际上为"基本翻译 重新输入"
			blockName[writeBlockp] = BASIC_TRANSLATE;
			blockData[writeBlockp] = "";
			blockHTMLData[writeBlockp] = "";
			
			//单词和读音所在div
			Elements wandp = doc.select(
					String.format("div:eq(%d)",WORD_AND_PRON_DIV));
			//如果有单词和读音
			if(wandp.isEmpty()){
				blockData[writeBlockp] = wandp.text();
				blockHTMLData[writeBlockp] = wandp.html();
			}
			
			Element contentFirst = contents.first();
			blockData[writeBlockp] = 
					blockData[writeBlockp].concat(contentFirst.text());
			blockHTMLData[writeBlockp] = 
					blockHTMLData[writeBlockp].concat(contentFirst.html());
			
			writeBlockp++;
		}
		
		//写入剩下的数据块
		for( ; writeBlockp < categoryLength ; writeBlockp++){
			blockName[writeBlockp] = categorys.eq(writeBlockp).text();
			blockData[writeBlockp] = contents.eq(writeBlockp).text();
			blockHTMLData[writeBlockp] = contents.eq(writeBlockp).html();
		}
		
		res = new YdDictResponse(blockName, blockData, blockHTMLData, categoryLength);
		return res;
	}
	
	/**
	 * 通过有道的singledict网页获取更加详细的单词翻译内容
	 * @param word 需要翻译的单词或中文
	 * @return 翻译结果
	 */
	public static YdDictResponse detailedTranslate(String word){
		return detailedTranslate(word, YdDetailedDictName.getAllDictName());
	}
	
	/**
	 * 通过有道的singledict网页获取更加详细的单词翻译内容
	 * @param word 需要翻译的单词或中文
	 * @param dictName 指定需要获取的详细内容词典
	 * @return 翻译结果
	 */
	public static YdDictResponse detailedTranslate(String word,YdDetailedDictName dictName){
		//返回的结果的对象
		YdDictResponse res = null;
		
		//去除需要翻译内容的无效字符
		word = word.trim();
		//无查询内容
		if(word.length() == 0){
			res = new YdDictResponse(new String[]{},
					new String[]{}, new String[]{}, 0);
			return res;
		}
		
		//获得查询网址
		String[] urls = genQueryDetailedURL(word, dictName);
		//查询网址数组的长度
		int urlsLength = urls.length;
		//JSoup可解析的响应内容
		Connection.Response response = null;
		
		//各个数据块数组
		String[] blockName = new String[urlsLength];
		String[] blockData = new String[urlsLength];
		String[] blockHTMLData = new String[urlsLength];
		//写入数据的指针
		int writeBlockp = 0;
		//临时存放在响应中获得的HTML代码
		String tmpHTML = null;
		
		//初始化详细词典名称查询指针
		dictName.initGet();

		//循环获得查询网址
		for(String url : urls){
			response = genJSoupResponse(url);
			if(response == null){
				res = new YdDictResponse("网络连接错误!");
				return res;
			}
			
			blockName[writeBlockp] = dictName.getNext();
			blockHTMLData[writeBlockp] = tmpHTML = response.body();
			blockData[writeBlockp] = Jsoup.parse(tmpHTML).text();
			
			writeBlockp++;
		}
		
		res = new YdDictResponse(blockName, blockData, blockHTMLData, writeBlockp);
		return res;
	}
	
	/**
	 * 获得读音音频数据到文件
	 * @param word 需要获得音频数据的单词或中文
	 * @param lang 音频数据的发音国家
	 * @param saveFile 保存的文件
	 * @return 是否获取保存成功
	 */
	public static boolean getPronounce(String word,PronounceLang lang,File saveFile){
		//获得数据数组
		byte[] dat = getPronounce(word, lang);
		//数据是否为空
		if(dat.length != 0){
			FileOutputStream os = null;			
			try {
				//写入数据
				os = new FileOutputStream(saveFile);
				os.write(dat);
				os.close();
			} catch (IOException e) {
				YdErrLog.err("写入音频数据到文件时出现IO错误!");
				return false;
			}
			return true;
		}else{
			return false;
		}
	}
	
	/**
	 * 获得读音音频数据的byte数组
	 * @param word 需要获得音频数据的单词或中文
	 * @param lang 音频数据的发音国家
	 * @return 读音音频数据的byte数组,获取失败则返回空数组byte[0]
	 */
	public static byte[] getPronounce(String word,PronounceLang lang){
		//返回数据
		byte[] dat = new byte[0];
		
		//去除需要翻译内容的无效字符
		word = word.trim();
		//无查询内容
		if(word.length() == 0){
			return dat;
		}
		
		//获得查询URL
		String urlStr = genQueryPronounceURL(word,lang);
		URL url = null;
		//HTTP链接对象
		HttpURLConnection con = null;
		//返回数据长度
		int contentLength = 0;
		//读取返回数据流
		InputStream is = null;
		try {
			url = new URL(urlStr);
			//打开链接
			con =(HttpURLConnection) url.openConnection();
			//获得数据长度
			contentLength = con.getContentLength();
			//音频无效
			if(contentLength <= PRONOUNCE_VOID_LENGTH){
				return dat;
			}
			//读取数据
			dat = new byte[contentLength];
			is = con.getInputStream();
			is.read(dat);
			//关闭链接
			is.close();
			con.disconnect();
		} catch (MalformedURLException e) {
			YdErrLog.err("获得发音数据时,生成URL对象出错!");
			return dat;
		} catch (IOException e) {
			YdErrLog.err("获得发音数据时,出现网络或者其他IO错误!");
			return dat;
		}
		return dat;
	}
	
	/**
	 * 通过URL获得JSoup可解析的文档对象
	 * @param url URL
	 * @return JSoup可解析的文档对象
	 */
	private static Document genJSoupDocument(String url){
		Document doc = null;
		try {
			doc = Jsoup.connect(url).userAgent(HTTP_USERAGENT).get();
		} catch (IOException e) {
			YdErrLog.err("IOException:\n 网络连接以及读写出错,无法获得翻译结果页面!");
		}
		return doc;
	}
	
	/**
	 * 生成请求数据的URL
	 * @param qword 单词
	 * @return URL
	 */
	private static String genQueryURL(String qword){
		try {
			//将单词转换成URL码
			qword = URLEncoder.encode(qword, QUERY_URLENCODER_CHARSET);
		} catch (UnsupportedEncodingException e) {
			YdErrLog.err("UnsupportedEncodingException:\n生成URL时,转换搜索关键词为URL码出错!");
		}
		return String.format(BASE_QUERY_URL, qword);
	}
	
	/**
	 * 生成发音数据的URL
	 * @param qword 单词
	 * @param lang 发音语言
	 * @return URL
	 */
	private static String genQueryPronounceURL(String qword,PronounceLang lang){
		try {
			//将单词转换成URL码
			qword = URLEncoder.encode(qword, QUERY_URLENCODER_CHARSET);
		} catch (UnsupportedEncodingException e) {
			YdErrLog.err("UnsupportedEncodingException:\n生成URL时,转换搜索关键词为URL码出错!");
		}
		return String.format(
				BASE_QUERY_PRONOUNCE_URL, qword,lang == PronounceLang.BRITISH?1:2);
	}
	
	/**
	 * 生成请求详细内容词典的URL
	 * @param qword 单词
	 * @param dictName 词典名称列表类
	 * @return 一堆URL
	 */
	private static String[] genQueryDetailedURL(String qword,YdDetailedDictName dictName){
		try {
			//将单词转换成URL码
			qword = URLEncoder.encode(qword, QUERY_URLENCODER_CHARSET);
		} catch (UnsupportedEncodingException e) {
			YdErrLog.err("UnsupportedEncodingException:\n生成URL时,转换搜索关键词为URL码出错!");
		}
		String[] result = new String[dictName.length()];
		dictName.initGet();
		for(int i = 0 ; i < result.length ; i++){
			result[i] = String.format(BASE_QUERY_DETAILED_URL,
					dictName.getNext(),qword);
		}
		return result;
	}

	
	/**
	 * 通过URL获得JSoup可解析的响应内容
	 * @param url URL
	 * @return JSoup可解析的响应内容
	 */
	private static Connection.Response genJSoupResponse(String url){
		Connection.Response res = null;
		try {
			res = Jsoup.connect(url).execute();
			if(res.statusCode() != 200){
				YdErrLog.err("HTTP状态码有误,无法获得详细内容结果!(HTTP状态码:"
						.concat(Integer.toString(res.statusCode()).concat(")")));
				res = null;
			}
		} catch (IOException e) {
			YdErrLog.err("IOException:\n 网络连接以及读写出错,无法获得详细内容结果!");
		}
		return res;
	}

	/**
	 * 获得发音时选择的语言
	 * 
	 * @author VoyageLiu
	 * @version 2.0
	 */
	public static enum PronounceLang{
		BRITISH,
		AMERICAN
	}
}